<?php

class apns
{
private $apnsData;
private $showErrors = true;
private $logErrors = true;
private $logPath;
private $logMaxSize = 1048576;
private $certificate;
private $ssl = 'ssl://gateway.push.apple.com:2195';
private $feedback = 'ssl://feedback.push.apple.com:2196';
private $sandboxCertificate;
private $sandboxSsl = 'ssl://gateway.sandbox.push.apple.com:2195';
private $sandboxFeedback = 'ssl://feedback.sandbox.push.apple.com:2196';
private $message;
function __construct($args=NULL,$certificate=NULL,$sandboxCertificate=NULL,$logPath=NULL)
{
if(!empty($certificate) &&file_exists($certificate))
$this->certificate = $certificate;
else
$this->certificate = APP_ROOT_PATH.'system/pem/apns.pem';
if(!empty($sandboxCertificate) &&file_exists($sandboxCertificate))
$this->sandboxCertificate = $sandboxCertificate;
else
$this->sandboxCertificate = APP_ROOT_PATH.'system/pem/apns-dev.pem';
$this->checkSetup();
$this->apnsData = array(
'production'=>array(
'certificate'=>$this->certificate,
'ssl'=>$this->ssl,
'feedback'=>$this->feedback
),
'sandbox'=>array(
'certificate'=>$this->sandboxCertificate,
'ssl'=>$this->sandboxSsl,
'feedback'=>$this->sandboxFeedback
)
);
if ($logPath !== null)
$this->logPath = $logPath;
else
$this->logPath = APP_ROOT_PATH.'public/logger/apns.log';
if(!empty($args)){
switch($args['task']){
case 'register':
$this->_registerDevice(
$args['appname'],
$args['appversion'],
$args['deviceuid'],
$args['devicetoken'],
$args['devicename'],
$args['devicemodel'],
$args['deviceversion'],
$args['pushbadge'],
$args['pushalert'],
$args['pushsound'],
$args['clientid']
);
break;
case 'fetch';
$this->_fetchMessages();
break;
default:
echo "No APNS Task Provided...\n";
break;
}
}
}
private function checkSetup(){
if(!file_exists($this->certificate)) $this->_triggerError('Missing Production Certificate.',E_USER_ERROR);
if(!file_exists($this->sandboxCertificate)) $this->_triggerError('Missing Sandbox Certificate.',E_USER_ERROR);
clearstatcache();
$certificateMod = substr(sprintf('%o',fileperms($this->certificate)),-3);
$sandboxCertificateMod = substr(sprintf('%o',fileperms($this->sandboxCertificate)),-3);
}
private function _registerDevice($appname,$appversion,$deviceuid,$devicetoken,$devicename,$devicemodel,$deviceversion,$pushbadge,$pushalert,$pushsound,$clientid = NULL){
if(strlen($appname)==0) $this->_triggerError('Application Name must not be blank.',E_USER_ERROR);
else if(strlen($appversion)==0) $this->_triggerError('Application Version must not be blank.',E_USER_ERROR);
else if(strlen($deviceuid)>40) $this->_triggerError('Device ID may not be more than 40 characters in length.',E_USER_ERROR);
else if(strlen($devicetoken)!=64) $this->_triggerError('Device Token must be 64 characters in length.',E_USER_ERROR);
else if(strlen($devicename)==0) $this->_triggerError('Device Name must not be blank.',E_USER_ERROR);
else if(strlen($devicemodel)==0) $this->_triggerError('Device Model must not be blank.',E_USER_ERROR);
else if(strlen($deviceversion)==0) $this->_triggerError('Device Version must not be blank.',E_USER_ERROR);
else if($pushbadge!='disabled'&&$pushbadge!='enabled') $this->_triggerError('Push Badge must be either Enabled or Disabled.',E_USER_ERROR);
else if($pushalert!='disabled'&&$pushalert!='enabled') $this->_triggerError('Push Alert must be either Enabled or Disabled.',E_USER_ERROR);
else if($pushsound!='disabled'&&$pushsound!='enabled') $this->_triggerError('Push Sount must be either Enabled or Disabled.',E_USER_ERROR);
$sql = 'INSERT INTO '.DB_PREFIX."apns_devices  
				VALUES (
					NULL,
					'{$clientid}',
					'{$appname}',
					'{$appversion}',
					'{$deviceuid}',
					'{$devicetoken}',
					'{$devicename}',
					'{$devicemodel}',
					'{$deviceversion}',
					'{$pushbadge}',
					'{$pushalert}',
					'{$pushsound}',
					'production',
					'active',
					NOW(),
					NOW()
				)
				ON DUPLICATE KEY UPDATE
				`devicetoken`='{$devicetoken}',
				`devicename`='{$devicename}',
				`devicemodel`='{$devicemodel}',
				`deviceversion`='{$deviceversion}',
				`pushbadge`='{$pushbadge}',
				`pushalert`='{$pushalert}',
				`pushsound`='{$pushsound}',
				`status`='active',
				`modified`=NOW();";
$GLOBALS['db']->query($sql);
}
private function _unregisterDevice($token){
$sql = 'UPDATE '.DB_PREFIX."apns_devices 
				SET `status`='uninstalled'
				WHERE `devicetoken`='{$token}'
				LIMIT 1;";
$GLOBALS['db']->query($sql);
}
private function _fetchMessages()
{
$bln = 0;
$sql = 'SELECT
				`apns_messages`.`pid`,
				`apns_messages`.`message`,
				`apns_devices`.`devicetoken`,
				`apns_devices`.`development`
			FROM '.DB_PREFIX.'apns_logs as apns_messages
			LEFT JOIN '.DB_PREFIX."apns_devices as apns_devices ON (`apns_devices`.`pid` = `apns_messages`.`fk_device` AND `apns_devices`.`clientid` = `apns_messages`.`clientid`)
			WHERE `apns_messages`.`status`='queued'
				AND `apns_messages`.`delivery` <= NOW()
				AND `apns_devices`.`status`='active'
			GROUP BY `apns_messages`.`fk_device`
			ORDER BY `apns_messages`.`created` ASC
			LIMIT 1;";
$res = $GLOBALS['db']->getAll($sql);
foreach($res as $row)
{
$bln = 1;
$pid = $this->prepare($row['pid']);
$message = stripslashes($this->prepare($row['message']));
$token = $this->prepare($row['devicetoken']);
$development = $this->prepare($row['development']);
$this->_pushMessage($pid,$message,$token,$development);
}
return $bln;
}
private function _pushMessage($pid,$message,$token,$development){
if(strlen($pid)==0) $this->_triggerError('Missing message pid.',E_USER_ERROR);
if(strlen($message)==0) $this->_triggerError('Missing message.',E_USER_ERROR);
if(strlen($token)==0) $this->_triggerError('Missing message token.',E_USER_ERROR);
if(strlen($development)==0) $this->_triggerError('Missing development status.',E_USER_ERROR);
$ctx = stream_context_create();
stream_context_set_option($ctx,'ssl','local_cert',$this->apnsData[$development]['certificate']);
$fp = stream_socket_client($this->apnsData[$development]['ssl'],$error,$errorString,100,(STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT),$ctx);
if(!$fp){
$this->_pushFailed($pid);
$this->_triggerError("Failed to connect to APNS: {$error} {$errorString}.");
}
else {
$msg = chr(0).pack('n',32).pack('H*',$token).pack('n',strlen($message)).$message;
$fwrite = fwrite($fp,$msg);
if(!$fwrite) {
$this->_pushFailed($pid);
$this->_triggerError('Failed writing to stream.',E_USER_ERROR);
}
else {
$this->_pushSuccess($pid);
}
}
fclose($fp);
$this->_checkFeedback($development);
}
private function _checkFeedback($development){
$ctx = stream_context_create();
stream_context_set_option($ctx,'ssl','local_cert',$this->apnsData[$development]['certificate']);
stream_context_set_option($ctx,'ssl','verify_peer',false);
$fp = stream_socket_client($this->apnsData[$development]['feedback'],$error,$errorString,100,(STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT),$ctx);
if(!$fp) $this->_triggerError("Failed to connect to device: {$error} {$errorString}.");
while ($devcon = fread($fp,38)){
$arr = unpack('H*',$devcon);
$rawhex = trim(implode('',$arr));
$token = substr($rawhex,12,64);
if(!empty($token)){
$this->_unregisterDevice($token);
$this->_triggerError("Unregistering Device Token: {$token}.");
}
}
fclose($fp);
}
private function _pushSuccess($pid){
$sql = 'UPDATE '.DB_PREFIX."apns_logs  
				SET `status`='delivered'
				WHERE `pid`={$pid}
				LIMIT 1;";
$GLOBALS['db']->query($sql);
}
private function _pushFailed($pid){
$sql = 'UPDATE '.DB_PREFIX."apns_logs  
				SET `status`='failed'
				WHERE `pid`={$pid}
				LIMIT 1;";
$GLOBALS['db']->query($sql);
}
private function _triggerError($error,$type=E_USER_NOTICE){
$backtrace = debug_backtrace();
$backtrace = array_reverse($backtrace);
$error .= "\n";
$i=1;
foreach($backtrace as $errorcode){
$file = ($errorcode['file']!='') ?'-> File: '.basename($errorcode['file']).' (line '.$errorcode['line'].')':'';
$error .= "\n\t".$i.') '.$errorcode['class'].'::'.$errorcode['function']." {$file}";
$i++;
}
$error .= "\n\n";
if($this->logErrors &&file_exists($this->logPath)){
if(filesize($this->logPath) >$this->logMaxSize) $fh = fopen($this->logPath,'w');
else $fh = fopen($this->logPath,'a');
fwrite($fh,$error);
fclose($fh);
}
if($this->showErrors) trigger_error($error,$type);
}
private function _jsonEncode($array=false){
if(function_exists('json_encode')){
return json_encode($array);
}
if(is_null($array)) return 'null';
if($array === false) return 'false';
if($array === true) return 'true';
if(is_scalar($array)){
if(is_float($array)){
return floatval(str_replace(',','.',strval($array)));
}
if(is_string($array)){
static $jsonReplaces = array(array("\\",'/',"\n","\t","\r","\b","\f",'"'),array('\\\\','\\/','\\n','\\t','\\r','\\b','\\f','\"'));
return '"'.str_replace($jsonReplaces[0],$jsonReplaces[1],$array) .'"';
}
else return $array;
}
$isList = true;
for($i=0,reset($array);$i<count($array);$i++,next($array)){
if(key($array) !== $i){
$isList = false;
break;
}
}
$result = array();
if($isList){
foreach($array as $v) $result[] = $this->_jsonEncode($v);
return '['.join(',',$result) .']';
}
else {
foreach ($array as $k =>$v) $result[] = $this->_jsonEncode($k).':'.$this->_jsonEncode($v);
return '{'.join(',',$result) .'}';
}
}
public function newMessage($fk_device=NULL,$delivery=NULL,$clientId=NULL){
if(isset($this->message)){
unset($this->message);
$this->_triggerError('An existring message already created but not delivered. The previous message has been removed. Use queueMessage() to complete a message.');
}
if (is_null($fk_device))
{
$sql = 'SELECT `pid` FROM '.DB_PREFIX."apns_devices WHERE `status`='active'";
if (!is_null($clientId))
$sql .= " AND `clientid` = '{$this->prepare($clientId)}'";
$ids = array();
$res = $GLOBALS['db']->getAll($sql);
foreach ($res as $row)
{
$ids[] = $row['pid'];
}
$fk_device = $ids;
}
$this->message = array();
$this->message['aps'] = array();
$this->message['aps']['clientid'] = $clientId;
$this->message['send']['to'] = $fk_device;
$this->message['send']['when'] = $delivery;
}
public function queueMessage($message_id){
if (!isset($this->message))
$this->_triggerError('You cannot Queue a message that has not been created. Use newMessage() to create a new message.');
$to = $this->message['send']['to'];
$when = $this->message['send']['when'];
$clientId = is_null($this->message['aps']['clientid']) ?null : $this->prepare($this->message['aps']['clientid']);
$list = (is_array($to)) ?$to : array($to);
unset($this->message['send']);
foreach ($list as $key =>$val)
if (!is_numeric($val)) {
$this->_triggerError("TO id was not an integer: $val.");
unset($list[$key]);
}
if (empty($list))
$this->_triggerError('No valid recipient was provided.');
$sql = '
			SELECT `pid`, `pushbadge`, `pushalert`, `pushsound`, `clientid`
			FROM '.DB_PREFIX.'apns_devices 
			WHERE `pid` IN ('.implode(', ',$list) .")
				AND `status`='active'".(is_null($clientId) ?'': "
				AND `clientid` = '{$clientId}'");
$res = $GLOBALS['db']->getAll($sql);
if (count($res) == 0)
$this->_triggerError('This user does not exist in the database. Message will not be delivered.');
foreach ($res as $row)
{
$deliver = true;
$deviceid = $row['pid'];
$pushbadge = $this->prepare($row['pushbadge']);
$pushalert = $this->prepare($row['pushalert']);
$pushsound = $this->prepare($row['pushsound']);
$clientId = $this->prepare($row['clientid']);
if($pushbadge=='disabled'&&$pushalert=='disabled'&&$pushsound=='disabled')
$deliver = false;
if($deliver===false &&FDB::numRows($res) >0) {
$this->_triggerError('This user has disabled all push notifications. Message will not be delivered.');
}
else if($deliver===true) {
$usermessage = $this->message;
if($pushbadge=='disabled'){
$this->_triggerError('This user has disabled Push Badge Notifications, Badge will not be delivered.');
unset($usermessage['aps']['badge']);
}
if($pushalert=='disabled'){
$this->_triggerError('This user has disabled Push Alert Notifications, Alert will not be delivered.');
unset($usermessage['aps']['alert']);
}
if($pushsound=='disabled'){
$this->_triggerError('This user has disabled Push Sound Notifications, Sound will not be delivered.');
unset($usermessage['aps']['sound']);
}
$fk_device = $this->prepare($deviceid);
$message = $this->_jsonEncode($usermessage);
$message = $this->prepare($message);
$delivery = (!empty($when)) ?"'{$when}'":'NOW()';
$sql = 'INSERT INTO '.DB_PREFIX."apns_logs  
						VALUES (
							0,
							'{$clientId}',
							'{$fk_device}',
							'{$message}',
							{$delivery},
							'queued',
							".get_gmtime().',
							'.get_gmtime().",
							$message_id
						);";
$GLOBALS['db']->query($sql);
unset($usermessage);
}
}
unset($this->message);
}
public function addMessageAlert($alert=NULL,$actionlockey=NULL,$lockey=NULL,$locargs=NULL){
if(!$this->message) $this->_triggerError('Must use newMessage() before calling this method.',E_USER_ERROR);
if(isset($this->message['aps']['alert'])){
unset($this->message['aps']['alert']);
$this->_triggerError('An existring alert was already created but not delivered. The previous alert has been removed.');
}
switch(true){
case (!empty($alert) &&empty($actionlockey) &&empty($lockey) &&empty($locargs)):
if(!is_string($alert)) $this->_triggerError('Invalid Alert Format. See documentation for correct procedure.',E_USER_ERROR);
$this->message['aps']['alert']['body'] = $alert;
break;
case (!empty($alert) &&!empty($actionlockey) &&empty($lockey) &&empty($locargs)):
if(!is_string($alert)) $this->_triggerError('Invalid Alert Format. See documentation for correct procedure.',E_USER_ERROR);
else if(!is_string($actionlockey)) $this->_triggerError('Invalid Action Loc Key Format. See documentation for correct procedure.',E_USER_ERROR);
$this->message['aps']['alert']['body'] = $alert;
$this->message['aps']['alert']['action-loc-key'] = $actionlockey;
break;
case (empty($alert) &&empty($actionlockey) &&!empty($lockey) &&!empty($locargs)):
if(!is_string($lockey)) $this->_triggerError('Invalid Loc Key Format. See documentation for correct procedure.',E_USER_ERROR);
$this->message['aps']['alert']['body'] = '';
$this->message['aps']['alert']['loc-key'] = $lockey;
$this->message['aps']['alert']['loc-args'] = $locargs;
break;
default:
$this->_triggerError('Invalid Alert Format. See documentation for correct procedure.',E_USER_ERROR);
break;
}
}
public function addMessageBadge($number=NULL){
if(!$this->message) $this->_triggerError('Must use newMessage() before calling this method.',E_USER_ERROR);
if($number) {
if(isset($this->message['aps']['badge'])) $this->_triggerError('Message Badge has already been created. Overwriting with '.$number.'.');
$this->message['aps']['badge'] = (int)$number;
}
}
public function addMessageCustom($key=NULL,$value=NULL){
if(!$this->message) $this->_triggerError('Must use newMessage() before calling this method.',E_USER_ERROR);
if(!empty($key) &&!empty($value)) {
if(isset($this->message[$key])){
unset($this->message[$key]);
$this->_triggerError('This same Custom Key already exists and has not been delivered. The previous values have been removed.');
}
if(!is_string($key)) $this->_triggerError('Invalid Key Format. Key must be a string. See documentation for correct procedure.',E_USER_ERROR);
$this->message[$key] = $value;
}
}
public function addMessageSound($sound=NULL){
if(!$this->message) $this->_triggerError('Must use newMessage() before calling this method.',E_USER_ERROR);
if($sound) {
if(isset($this->message['aps']['sound'])) $this->_triggerError('Message Sound has already been created. Overwriting with '.$sound.'.');
$this->message['aps']['sound'] = (string)$sound;
}
}
public function processQueue(){
return $this->_fetchMessages();
}
function prepare($unescaped_string,$do_like = false)
{
if ($do_like)
{
$unescaped_string = str_replace(array('%','_'),array('\%','\_'),$unescaped_string);
}
if (PHP_VERSION >= '4.3')
{
return mysql_real_escape_string($unescaped_string);
}
else
{
return mysql_escape_string($unescaped_string);
}
}
}
?>